/*   
 *  Copyright 1997-2004 The Apache Sofware Foundation.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package org.apache.tomcat.util.depend;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;

import org.apache.tomcat.util.compat.Jdk11Compat;

/**
 * This is a wrapper class loader that will delegate all calls to
 * the parent. It will also generate events for every loaded class,
 * for use in maintaining dependencies.
 *
 * In order to keep this generic we'll use findResource() to find the
 * source of the class, and then forward to the class loader - that
 * means we duplicate the search operation.
 * 
 * Class loading happens only once per request, and this will have probably
 * little effect.
 * Also, the alternative is to use custom class loaders - there are many
 * reasons to avoid this.
 *
 * In "production" sites reloading should be turned off anyway, so the
 * class loader will not be "wrapped"
 * 
 */
public class DependClassLoader extends ClassLoader {
    static org.apache.commons.logging.Log logger =
	org.apache.commons.logging.LogFactory.getLog(DependClassLoader.class);

    protected ClassLoader parent;
    protected ClassLoader parent2;
    
    private static int debug=0;
    DependManager dependM;
    protected Object pd;
    static Jdk11Compat jdkCompat=Jdk11Compat.getJdkCompat();

    public static interface DCLFactory {
        public ClassLoader createDependLoader(DependManager depM, ClassLoader parent, Object pd, int debug);
    }
    
    public static ClassLoader getDependClassLoader( DependManager depM,
                                                    ClassLoader parent,
                                                    Object pd, int debug ) {
	if( jdkCompat.isJava2() ) {
	    try {
		Class c=Class.forName( "org.apache.tomcat.util.depend.DependClassLoader12");
		DCLFactory dcl=(DCLFactory)c.newInstance();
                return dcl.createDependLoader( depM, parent, pd, debug );
	    } catch(Exception ex ) {
		ex.printStackTrace();
	    }
	} 
	return new DependClassLoader( depM, parent, pd );
    }

    DependClassLoader() {
    }
    
    public DependClassLoader( DependManager depM, ClassLoader parent, Object pd ) {
	super(); // will check permissions
	init( depM, parent, pd );
    }

    void init(  DependManager depM, ClassLoader parent, Object pd ) {
	this.parent=parent;
	this.parent2=jdkCompat.getParentLoader( parent );
	dependM=depM;
	this.pd=pd;
    }

    /**
     * Resolves the specified name to a Class. The method loadClass()
     * is called by the virtual machine.  As an abstract method,
     * loadClass() must be defined in a subclass of ClassLoader.
     *
     * @param      name the name of the desired Class.
     * @param      resolve true if the Class needs to be resolved;
     *             false if the virtual machine just wants to determine
     *             whether the class exists or not
     * @return     the resulting Class.
     * @exception  ClassNotFoundException  if the class loader cannot
     *             find a the requested class.
     */
    protected synchronized Class loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
	return loadClassInternal1( name, resolve );
    }

    /** Actual class loading. The name 'loadClassInternal' generates a warning,
     *  as a private method with the same name exists int ClassLoader in JDK1.1 ( Sun impl ).
     */
    protected Class loadClassInternal1( String name, boolean resolve )
	throws ClassNotFoundException
    {
	if( logger.isTraceEnabled() ) 
	    logger.trace( "loadClass() " + name + " " + resolve);
	// The class object that will be returned.
        Class c = null;

	// check if  we already loaded this class
	c = findLoadedClass( name );
	if (c!= null ) {
	    if(resolve) resolveClass(c);
	    return c;
        }

        String classFileName = name.replace('.', '/' ) + ".class";

	URL res=getResource( classFileName );

	// If it's in parent2, load it ( we'll not track sub-dependencies ).
	try {
	    c = parent2.loadClass(name);
	    if (c != null) {
		if (resolve) resolveClass(c);
		// No need, we can't reload anyway
		// dependency( c, res );
		return c;
	    }
	} catch (Exception e) {
	    c = null;
	}

	if( res==null ) 
	    throw new ClassNotFoundException(name);

	// This should work - SimpleClassLoader should be able to get
	// resources from jar files. 
	InputStream is=getResourceAsStream( classFileName );
	if( is==null ) 
	    throw new ClassNotFoundException(name);


	// It's in our parent. Our task is to track all class loads, the parent
	// should load anything ( otherwise the deps are lost ), but just resolve
	// resources.
	byte data[]=null;
	try {
	    if( is.available() > 0) {
		data=readFully( is );
		if( data.length==0 ) data=null;
	    }
	    is.close();
	} catch(IOException ex ) {
	    if( logger.isDebugEnabled() )
		logger.debug(" error reading " + name, ex);
	    data=null;
	    throw new ClassNotFoundException( name + " error reading " + ex.toString());
	}
	if( data==null ) 
	    throw new ClassNotFoundException( name + " lenght==0");

	c=defineClassCompat( name, data, 0, data.length, res );
	dependency( c, res );
	
	if (resolve) resolveClass(c);

	return c;
    }

    /** This method must be overriden to provide additional functionality,
	like associating a protection domain
    */
    protected Class defineClassCompat( String name, byte data[], int s, int end, URL res )
	throws ClassNotFoundException
    {
	return defineClass(data, s, end);
    }
    
    public URL getResource(String name) {
	return parent.getResource(name);
    }

    public InputStream getResourceAsStream(String name) {
	return parent.getResourceAsStream( name );
    }

    private void dependency( Class c, URL res ) {
	if( res==null) return;
	File f=null;
	if( "file".equals( res.getProtocol() )) {
	    f=new File( res.getFile());
	    if( logger.isTraceEnabled() ) 
		logger.trace( "File dep "  +f );
	    if( ! f.exists()) f=null;
	}
	if( "jar".equals( res.getProtocol() )) {
	    String fileN=res.getFile();
	    int idx=fileN.indexOf( "!" );
	    if( idx>=0 )
		fileN=fileN.substring( 0, idx) ;
	    // Bojan Smojver <bojan@binarix.com>: remove jar:
	    if( fileN.startsWith( "file:" ))
		fileN=fileN.substring( 5 );
	    // If the standard URL parser is used ( jdk1.1 compat )
	    if( fileN.startsWith( "/file:" ))
		fileN=fileN.substring( 6 );
	    f=new File(fileN);
	    if( logger.isTraceEnabled() ) 
		logger.trace( "Jar dep "  +f + " " + f.exists() );
	    if( ! f.exists()) f=null;
	}

	if( f==null ) return;
	Dependency dep=new Dependency();
	dep.setLastModified( f.lastModified() );
	dep.setTarget( c );
	dep.setOrigin( f );

	dependM.addDependency( dep );
    }

    public ClassLoader getParentLoader() {
        return parent;
    }

    private byte[] readFully( InputStream is )
	throws IOException
    {
	byte b[]=new byte[1024];
	int count=0;

	int available=1024;
	
	while (true) {
	    int nRead = is.read(b,count,available);
	    if( nRead== -1 ) {
		// we're done reading
		byte result[]=new byte[count];
		System.arraycopy( b, 0, result, 0, count );
		return result;
	    }
	    // got a chunk
	    count += nRead;
            available -= nRead;
	    if( available == 0 ) {
		// buffer full
		byte b1[]=new byte[ b.length * 2 ];
		available=b.length;
		System.arraycopy( b, 0, b1, 0, b.length );
		b=b1;
	    }
        }
    }
}
